其他
推开“微前端”的门
导读:“微前端”和“微服务”类似,是这两年被频繁提及的名词。web开发从前后端放在一起的单体应用,演进成前后端分离的SPA,这些改变让前后端实现了开发解耦、独立发布。解耦让开发、调试、发布的过程都更加自由灵活,但随着业务的发展,中大型的SPA逐渐成为了“巨石应用”(Monolithic Applications),当初因为前后端分离带来的“自由”也渐行渐远,模块的拆解越来越被需要。
全文5574字,预计阅读时间14分钟
思考什么样的系统或者前端需要微前端 简述微前端工程中需要关注的一些设计要点
深入介绍某些开源框架并对比 如何设计一个非常通用的微前端框架 针对某个设计点的实现方式非常详尽的介绍
一、什么样的系统或者前端团队需要微前端?
前端模块A、B、C、D均属于一个需要整体发布的SPA; 模块C上线依赖服务2、3,模块D依赖服务1、4; 因为模块C和D需要一起上线,和服务2毫无关系的模块D、服务1、4都需要等待服务2的发布。
并行开发的模块数量多
当你有多个功能相对解耦独立的模块,需要并行开发、发布。协同开发的人员数量多
当单个应用开发同学的人数超过一定规模(如大于10人),且每个人负责的子模块相对固定,人数增多协同效率往往会指数增长。协同开发的团队数量多
这是一个有趣的点,和2的区别,主要在“屁股决定脑袋”的“屁股”上。如果你们由于种种原因,不得不跨团队维护同一个工程,那你们可能面对着开发权限、规范、节奏等很多协同点,这些协同有时候会因为跨团队效率更低。发布频率高
这条需要与1和2结合起来看,人数多但是发布频率较低的模块,协同沟通等成本就成了伪命题。当然,多人协同开发的应用,大概率是发展中的业务,需要频繁的迭代。需要跨技术栈
这一点可能是很多团队选择微前端的重要理由。你们可能维护了一个经营多年、系统繁杂、技术栈较陈旧的系统,你希望能引入新的技术栈但又没有人力一下子重写系统。先把系统微前端化,再渐进式地分子模块重写,会是一个工作节奏可控、质量风险较低的办法。
但是,针对跨技术栈这个命题,对于单独产品或平台来说,我个人认为有个 「“陷阱”」 。从团队长远开发效率、平台的性能优化空间、体验一致等多种角度来看,长期不加管控的跨技术栈是有风险的。一些小粒度的抽象(组件、业务模块等)难以被高效复用,一些升级难以被直接应用到全局等。这些问题导致的效率下降,可能会掩盖独立开发、发布带来的效率提升。Martin Fowler在介绍微前端的收益之一时时也写的是 「Incremental upgrades」 ,渐进式更新不等同于永久区别。因此,除非你做的只是一个门户,对子模块的一致性没有很高的协同要求,我更建议跨技术栈是渐进式迁移的中间态。
二、选择微前端需要关注的设计点
2.1 主模块与子模块
subapp是一个iframe,最简单暴力的实现方式,同时有iframe实现页面的一切限制。优化空间、顶层控制力有限,个人不推荐。 subapp是web component,跟随路由切换实例化组件。你需要考虑浏览器的兼容性限制。 subapp是一个独立发布的子bundle。子bundle需要定义一些生命周期hook,如register、mount、unmount等。这个方法应该是比较普及使用的。
2.2 单实例 vs 多实例
const subAppRoutes = {
route1: 'https://your.static.server.com/app1/index.js',
route2: 'https://my.static.server.com/app2/index.js',
route3: 'https://other.static.server.com/app3/index.js'
};
const subAppRoutes = {
route1: [
{
subApp: 'https://your.static.server.com/app1/index.js',
layout: {
// 省去布局描述信息
}
},
{
subApp: 'https://my.static.server.com/app2/index.js'
}
],
route2: [
{
subApp: 'https://your.static.server.com/app2/index.js'
},
{
subApp: 'https://my.static.server.com/app3/index.js'
}
],
route3: [
{
subApp: 'https://my.static.server.com/app3/index.js'
}
]
}
2.3 子模块通信
全局数据共享
有个全局store,A将数据变更写入store,B监听store的change并做出响应,单向数据流的设计能让开发调试变的更加容易,当然你需要规避分模块对单一store内容的冲突问题,这个和路由冲突的解决方案类似,比如增加一些命名空间。剩下的内容和你接触过的各种单一store的设计都类同,不再赘述。事件通信
提供全局的EventBus能力,A派发事件,B接受事件并响应。这是个平平无奇的事件通信,但在微前端实现中有一个点需要关注。各子模块之间加载和实例化的过程大多是独立的、异步的,A发布事件时,B还没有实例化完成,那么这个消息可能会被漏掉。通过1中共享数据的方式可以解决大部分问题,如果你更喜欢用事件通信来解决,则需要在设计实现EventBus的时候考虑这个功能,例如缓存事件队列,当B在启用事件监听的时刻,回顾一下缓存事件队列中有没有已经派发且需要被响应的事件。
三、性能优化小贴士
3.1 多实例按需渲染
3.2 重复打包优化
Entry提供的全局实例。
你的顶层APP可以给每个实例化的子模块注入一个全局能力引用,提供一些几乎每个模块都要使用的能力,例如ajax请求能力、业务埋点监控能力等。好处是一些底层能力你可以很好地控制起来,缺点是子模块和entry之间的耦合会更深一些。如果你有一个子模块需要被应用在不同的entry APP中,那针对每个APP,你可能需要一个适配器层来屏蔽差异。抽取公共内容打包。
公共内容中最常见的就是polyfill了,每个模块通过使用「usage」(https://babeljs.io/docs/en/babel-preset-env#usebuiltins)单独打包,好处是开发、线上环境一致,缺点是polyfill内容会高度重复,如图五。
根据运行时环境实时加载polyfill,如polyfill.io。比较重的方案,个人觉得收益不一定非常划算。 webpack@5的「module federation」。还在beta阶段,建议生产环境慎重(如果你webpack升5之后工程还跑得起来的话)。 约定polyfill的白名单/黑名单。这个是我们工程中最后使用的方案,理由是轻量、稳妥,工程角度觉得“划算”。当然这个方式会有一个问题呼之欲出,单独开发的模块怎么保证使用的语法不会超出白名单?我们的解决方案是:为了在灵活性上有一定的规范约束,开发微服务子模块需要使用一个我们封装的dev cli workspace,在开发时完成App级别的变量注入、语法校验。发布编译阶段也会有相应的控制,因此「开发环境工具」也是微服务改造的利器。
3.3 自由 vs 规范
参考文献
Micro Frontends: https://martinfowler.com/articles/micro-frontends.html#IncrementalUpgrades @babel/preset-env · Babel: https://babeljs.io/docs/en/babel-preset-env#usebuiltins Polyfill.io: http://polyfill.io/ Module Federation | webpack: https://webpack.js.org/concepts/module-federation/
嘉宾介绍:
马海娜,百度商业平台研发部前端资深研发工程师,主要负责广告托管业务的前端架构。专注于在快速迭代的业务中打造高效、可扩展、体验良好的前端业务架构。爱好撸码和撸猫。